# -*- encoding: utf-8 -*-

module Opower
  module TimeSeries
    # Represents a query that can be sent to an OpenTSDB instance through a [TSDBClient] object.
    class Query
      attr_accessor :metrics, :request, :response, :format

      # Creates a new Query object.
      #
      # @param [Hash] config The configuration for this query.
      # @option config [String] :format The format to return data with. Defaults to 'json'.
      # @option config [String, Integer, DateTime] :start The start time. Required field.
      # @option config [String, Integer, DateTime] :end The end time. Optional field.
      # @option config [Hash] :m Array of metric hashes. This maps to what OpenTSDB expects as the metrics to query.
      #  * :aggregator [String] The aggregation type to utilize. Optional. Defaults to 'sum' if omitted.
      #  * :metric [String] The metric name. Required.
      #  * :tags [Hash] Hash consisting of Tag Key / Tag Value pairs. Optional.
      #  * :down_sample [Hash] to specify downsampling period and function
      #    * :period [String] The period of time to downsample one
      #    * :function [String] The function [min, max, sum, avg, dev]
      # @option config [Boolean] padding If set to true, OpenTSDB (>= 2.0) will pad the start/end period.
      #
      # This object also supports all of the options available to the REST API for OpenTSDB.
      # See http://opentsdb.net/http-api.html#/q_Parameters for more information.
      #
      # @return [Query] new Query object
      def initialize(config = {})
        @request = config
        @format = config.delete(:format)

        # Check that 'start' and 'm' parameters required by OpenTSDB are present
        @requirements = [:start, :m]
        validate_metrics

        @metrics = @request.delete(:m)
        check_metrics
        convert_dates

        # Create 'm' array - this is the
        @request[:m] = @metrics.map(&MetricQuery.method(:new))
      end

      # Returns the current query as a URL to a PNG generated by OpenTSDB
      #
      # @return [String] url to gnuplot graph
      def as_graph
        GraphingRequest.new(@request).to_s
      end

      private

      # Validates the query for the configured required fields. OpenTSDB requires that at the minimum the start time
      # and a metric field (defined by 'm') to be present.
      #
      # @raise [ArgumentError] thrown if 'm' or 'start' are missing
      def validate_metrics
        keys = @request.keys
        @requirements.each do |req|
          next if keys.include?(req.to_sym) || keys.include?(req.to_s)
          fail(ArgumentError, "#{req} is a required parameter.")
        end
      end

      # Converts dates to a format that OpenTSDB understands.
      def convert_dates
        @request = Hash[@request.map do |key, value|
          value = value.strftime('%Y/%m/%d-%H:%M:%S') if value.respond_to? :strftime
          [key.to_s, value]
        end]
      end

      # Checks the consistency of the metrics provided as defined in the user guide.
      #
      # @raise [ArgumentError] thrown if 'm' is in an unexpected state
      def check_metrics
        fail(ArgumentError, 'm parameter must be an array.') unless @metrics.is_a? Array
        fail(ArgumentError, 'm parameter must not be empty.') unless @metrics.length > 0

        @metrics.each do |metric|
          fail(ArgumentError, "Expected a Hash - got a #{metric.class}: '#{metric}'") unless metric.is_a? Hash
          fail(ArgumentError, 'Metric label must be present for query to run.') unless metric.key?(:metric)
        end
      end

      # Wrapper class for each query made against a separate metric.
      class MetricQuery
        # Initializes a wrapper object that represent a single metric that will be queried against in OpenTSDB.
        #
        # @param [Hash] metric a Hash representing a query against OpenTSDB
        def initialize(metric)
          @metric = metric.fetch(:metric)
          @tags = metric.fetch(:tags, {})
          @aggregator = metric.fetch(:aggregator, 'sum')
          @rate = metric.fetch(:rate, false)

          initialize_downsample(metric)
        end

        # Builds the query string representation of this MetricQuery object.
        #
        # @return [String] the URL query string that will be used for this metric query.
        def to_s
          str = "#{@aggregator}:"
          str << "#{@period}-#{@function}:" if @downsample
          str << 'rate:' if @rate
          str << "#{@metric}{#{build_tags}}"
          str
        end

        private

        # Initializes the down-sample setting on the metric
        #
        # @param [Hash] metric the metric hash
        def initialize_downsample(metric)
          @downsample = false
          return unless metric.key?(:downsample)

          down_sample = metric[:downsample]
          @period = down_sample[:period]
          @function = down_sample[:function]
          @downsample = true
        end

        # Builds the string representation of the tags for the metric
        #
        # @return query string for tag metrics
        def build_tags
          @tags.map { |key, value| "#{key}=#{value}" }.join(',')
        end
      end

      # Generates a graphing request URL from a properly configured Query object.
      class GraphingRequest
        # Initializes the graphing request wrapper using the data found in the Query object.
        #
        # @param [Hash] request query configuration
        def initialize(request)
          @parameters = request
          @request = []
          build_request
        end

        # Returns the URL for the specified query and its requested data.
        #
        # @return [String] URI address for the specified query
        def to_s
          URI.encode(@request.join('&'))
        end

        private

        # Builds the query string for the OpenTSDB REST API.
        # This method smells of :reek:NestedIterators
        #
        # @return [String] the GET query string for this object.
        def build_request
          @parameters.each_pair do |key, value|
            if value.respond_to? :each
              value.each { |array_element| @request << "#{key}=#{array_element.to_s.strip}" }
            else
              @request << "#{key}=#{value.to_s.strip}"
            end
          end
        end
      end
    end
  end
end
